﻿namespace HuwmanCode.Diagnostics.Logging

open System
open System.Diagnostics
open System.Reflection
open System.Runtime.Serialization
open System.ServiceModel
open System.ServiceModel.Channels
open System.ServiceModel.Dispatcher
open System.ServiceModel.Description

open HuwmanCode
open HuwmanCode.Reflection

[<AbstractClass>]
[<AttributeUsage(AttributeTargets.Class)>]
type ServiceErrorHandlerAttribute() =
    inherit Attribute()
    let mutable serviceType = Unchecked.defaultof<Type>

    static let exceptionInContract(serviceType:Type, error:Exception) =
        let serviceMethod =
            let wcfPrefix = "SyncInvoke"
            match error.StackTrace with
            | null -> null
            | st -> 
                let start = st.IndexOf(wcfPrefix, StringComparison.Ordinal)
                let trimmedTillMethod = st.Substring(start + wcfPrefix.Length)
                let parts = trimmedTillMethod.Split('(')
                parts.[0]

        let getFaults(mi:MethodInfo) =
            mi.GetCustomAttributes<FaultContractAttribute> (false)

        serviceType.GetInterfaces()
        |> Seq.map(fun i -> i.GetMethods()) |> Seq.concat
        |> Seq.filter(fun mi -> mi.Name = serviceMethod)
        |> Seq.map(fun mi -> mi |> getFaults) |> Seq.concat
        |> Seq.exists(fun faultAttrib -> faultAttrib.DetailType = error.GetType()) 

    abstract HandleError: error:exn -> unit

    interface IServiceBehavior with
        member this.Validate(description, host) = ()
        member this.AddBindingParameters(description, host, endpoints, parameters) = ()
        member this.ApplyDispatchBehavior(description, host) =
            serviceType <- description.ServiceType
            host.ChannelDispatchers 
            |> Seq.map(fun dispatcher -> dispatcher :?> ChannelDispatcher)
            |> Seq.iter(fun dispatcher -> dispatcher.ErrorHandlers.Add(this))

    interface IErrorHandler with
        member this.ProvideFault(error, version, fault) = 
            if not(exceptionInContract(serviceType, error)) then
                try
                    let errorType = error.GetType()
                    let faultUnboundedType = typeof<FaultException<_>>
                    let faultBoundedType = faultUnboundedType.MakeGenericType(errorType)
                    let constructorInfo = errorType.GetConstructor([|typeof<String>|])
                    Debug.Assert(not(constructorInfo = null), "Exception type " + errorType.ToString() + " does not have suitable constructor");

                    let exn = Activator.CreateInstance(errorType, error.Message) :?> Exception
                    let constructorInfo = faultBoundedType.GetConstructor([|exn.GetType()|])
                    Debug.Assert(not(constructorInfo = null), "Exception type " + errorType.ToString() + " does not have suitable constructor");

                    let faultException = Activator.CreateInstance(faultBoundedType, exn) :?> FaultException
                    let messageFault = faultException.CreateMessageFault()
                    fault <- Message.CreateMessage(version, messageFault, faultException.Action)
                with _ -> ()

        member this.HandleError(error) =
            try
                this.HandleError(error)
            with _ -> ()
            true